/**
 * jCarousel - Riding carousels with jQuery
 *
 * Written by Jan Sorgalla
 *   http://sorgalla.com
 *
 * Licensed under the MIT License
 *   http://www.opensource.org/licenses/mit-license.php
 *
 * Built on top of the jQuery library
 *   http://jquery.com
 *
 * Inspired by the "Carousel Component" by Bill Scott
 *   http://billwscott.com/carousel/
 */

jQuery.fn.extend({
    jcarousel: function(o) {
        return this.each(function() {
            new jQuery.jcarousel(this, o);
        });
    }
});

jQuery.jcarousel = function(c, o) {
    this.init(c, o);
};

jQuery.jcarousel.prototype = {

    o: {
        orientation: "horizontal",
        itemWidth: null,
        itemHeight: null,
        itemVisible: 3,
        itemScroll: null, // Will be set to itemVisible in init() if not passed
        scrollAnimation: "fast",
        autoScroll: 0,
        autoScrollStopOnInteract: true,
        wrap: false,
        loadItemHandler: null,
        nextButtonStateHandler: null,
        prevButtonStateHandler: null,
        itemFirstInHandler: null,
        itemFirstOutHandler: null,
        itemLastInHandler: null,
        itemLastOutHandler: null,
        itemVisibleInHandler: null,
        itemVisibleOutHandler: null,
        noButtons: false,
        buttonNextHTML: '<button>&gt;&gt;</button>',
        buttonPrevHTML: '<button>&lt;&lt;</button>'
    },

    init: function(c, o) {
        if (o) {
            jQuery.extend(this.o, o);
        }

        this.horiz = this.o.orientation == "vertical" ? false : true;

        this.scope = this.prepare(c);
        this.list  = jQuery("ul", this.scope).get(0) || jQuery("ol", this.scope).get(0);

        // Set itemScroll to itemVisible if not set
        this.o.itemScroll = this.o.itemScroll || this.o.itemVisible;
        this.autoTimer = null;

        this.size = jQuery("li", this.list).size();
        this.end = this.size;

        if (!this.o.itemWidth && this.size > 0) {
            var i = jQuery(jQuery("li", this.list).get(0));
            if (this.intval(i.css("width")) > 0) {
                this.o.itemWidth = this.intval(i.css("width"))
                                   + this.intval(i.css("marginLeft"))
                                   + this.intval(i.css("marginRight"))
                                   + this.intval(i.css("paddingLeft"))
                                   + this.intval(i.css("paddingRight"));
            }
        }

        this.o.itemWidth = this.o.itemWidth || 75;

        if (!this.o.itemHeight && this.size > 0) {
            var i = jQuery(jQuery("li", this.list).get(0));
            if (this.intval(i.css("height")) > 0) {
                this.o.itemHeight = this.intval(i.css("height"))
                                    + this.intval(i.css("marginTop"))
                                    + this.intval(i.css("marginBottom"))
                                    + this.intval(i.css("paddingTop"))
                                    + this.intval(i.css("paddingBottom"));
            }
        }

        this.o.itemHeight = this.o.itemHeight || 75;

        if (this.horiz) {
            this.itemD = this.o.itemWidth;
            var clipW  = this.o.itemWidth * this.o.itemVisible;
            var clipH  = this.o.itemHeight;
        } else {
            this.itemD = this.o.itemHeight;
            var clipW  = this.o.itemWidth;
            var clipH  = this.o.itemHeight * this.o.itemVisible;
        }

        jQuery(".jcarousel-clip", this.scope).css({
            zIndex: 3,
            padding: 0,
            margin: 0,
            width:  clipW + "px",
            height: clipH + "px",
            overflow: "hidden",
            position: "relative"
        });

        this.top  = this.intval(jQuery(this.list).css("top"));
        this.left = this.intval(jQuery(this.list).css("left"));

        jQuery(this.list).css({
            // "top" and "left" must be set, jQuery.animate() fails
            // otherwise on first animation
            zIndex: 1,
            position: "relative",
            top: this.top,
            left: this.left,
            margin: 0,
            padding: 0
        });

        var carousel = this;

        if (this.size > 0) {
            this.resize();

            var idx = 1;

            jQuery("li", this.list).each(function() {
                carousel.format(this, idx++);
            });
        }

        this.nextClick = function() { carousel.next(); };
        this.prevClick = function() { carousel.prev(); };

        // Possibly be set to hidden to hide the buttons from
        // browser with javascript disabled
        jQuery(".jcarousel-next", this.scope).show();
        jQuery(".jcarousel-prev", this.scope).show();
        this.buttons(false, false);

        this.first = this.o.itemVisible * -1 + 1;
        this.last  = 0;

        this.inAnimation = false;

        // Preload first items
        this.load(1, this.o.itemVisible);
        this.scroll(1);
        this.startAuto();
    },

    scope: function() {
        return this.scope;
    },

    get: function(idx) {
        return jQuery("li[@jCarouselItemIdx=" + idx + "]", this.list);
    },

    add: function(idx, s) {
        var item = this.get(idx);

        if (item.size() == 0) {
            var item = this.format(document.createElement("li"), idx);
            jQuery(this.list).append(item);
            this.size++;
            this.resize();
        }

        item.html(s);

        return item;
    },

    available: function(first, last) {
        if (this.end >= last) return true;

        this.end = last;
        return false;
    },

    loaded: function() {
        if (this.first > 1 && this.last < this.size) {
            this.buttons(true, true);
        } else if (this.first == 1 && this.last < this.size) {
            this.buttons(true, false);
        } else if (this.first > 1 && this.last >= this.size) {
            this.buttons(this.o.wrap, true);
        }
    },

    next: function() {
        this.stopAuto();

        if (this.o.autoScrollStopOnInteract) {
            this.disableAuto();
        }

        this.doNext();
    },

    prev: function() {
        this.stopAuto();

        if (this.o.autoScrollStopOnInteract) {
            this.disableAuto();
        }

        this.doPrev();
    },

    // --- Private ---

    startAuto: function() {
        if (this.o.autoScroll > 0) {
            var carousel = this;
            this.autoTimer = setTimeout(function() { carousel.doNext(); }, this.o.autoScroll * 1000);
        }
    },

    stopAuto: function() {
        if (this.autoTimer !== null) {
            clearTimeout(this.autoTimer);
            this.autoTimer = null;
        }
    },

    disableAuto: function() {
        this.stopAuto();
        this.o.autoScroll = 0;
    },

    resize: function() {
        if (this.size == 0) {
            return;
        }

        if (this.horiz) {
            var listW = this.size * this.itemD + 100;
            var listH = this.o.itemHeight;
        } else {
            var listW = this.o.itemWidth;
            var listH = this.size * this.itemD + 100;
        }

        jQuery(this.list).css({
            width:  listW + "px",
            height: listH + "px"
        });
    },

    format: function(item, idx) {
        return jQuery(item).css({
            float: "left",
            styleFloat: "left",
            width:  this.intval(jQuery(item).css("width")) ? jQuery(item).css("width") : this.o.itemWidth + "px",
            height: this.intval(jQuery(item).css("height")) ? jQuery(item).css("height") : this.o.itemHeight + "px",
            overflow: "hidden",
            listStyle: "none"
        }).attr("jCarouselItemIdx", idx);
    },

    scroll: function(idx) {
        if (this.inAnimation) return;
        this.inAnimation = false;

        var prevFirst = this.first;
        var prevLast  = this.last;

        idx = idx < 1 ? 1 : idx;

        var last = idx + this.o.itemVisible - 1;
        last = (last > this.size) ? this.size : last;

        var first = last - this.o.itemVisible + 1;
        first = (first < 1) ? 1 : first;

        last = first + this.o.itemVisible - 1;

        this.first = first;
        this.last  = last;

        // Preload next items
        this.load(this.last + 1, this.last + this.o.itemScroll);

        var pos = this.itemD * (this.first - 1) * -1;
        var arg = this.horiz ? {left: pos} : {top: pos};

        if (this.o.scrollAnimation) {
            this.inAnimation = true;
            var carousel = this;
            jQuery(this.list).animate(arg, this.o.scrollAnimation, function() {
               if (carousel.first == 1) {
                    // For some reason, jQuery.animate() don't wants
                    // to move the list to the startposition...
                    jQuery(carousel.list).css({
                        top: carousel.top,
                        left: carousel.left
                    });
                }
                carousel.inAnimation = false;
            });
        } else {
            jQuery(this.list).css(this.horiz ? "left" : "top", pos + "px");
        }

        this.trigger(prevFirst, prevLast, this.first, this.last);
    },

    trigger: function(prevFirst, prevLast, first, last) {
        var carousel = this;

        if (prevFirst != first) {
            if (this.o.itemFirstOutHandler != null) {
                var firstOutHandler = this.o.itemFirstOutHandler;
                this.get(prevFirst).each(function() {
                    firstOutHandler(carousel, this);
                });
            }

            if (this.o.itemFirstInHandler != null) {
                var firstInHandler = this.o.itemFirstInHandler;
                this.get(first).each(function() {
                    firstInHandler(carousel, this);
                });
            }
        }

        if (prevLast != last) {
            if (this.o.itemLastOutHandler != null) {
                var lastOutHandler = this.o.itemLastOutHandler;
                this.get(prevLast).each(function() {
                    lastOutHandler(carousel, this);
                });
            }

            if (this.o.itemLastInHandler != null) {
                var lastInHandler = this.o.itemLastInHandler;
                this.get(last).each(function() {
                    lastInHandler(carousel, this);
                });
            }
        }

        if (this.o.itemVisibleInHandler != null) {
            var visibleInHandler = this.o.itemVisibleInHandler;
            for (var i = first; i <= last; i++) {
                if (!(i >= prevFirst && i <= prevLast)) {
                    this.get(i).each(function() {
                         visibleInHandler(carousel, this);
                   });
                }
            }
        }

        if (this.o.itemVisibleOutHandler != null) {
            var visibleOutHandler = this.o.itemVisibleOutHandler;
            for (var i = prevFirst; i <= prevLast; i++) {
                if (!(i >= first && i <= last)) {
                    this.get(i).each(function() {
                        visibleOutHandler(carousel, this);
                    });
                }
            }
        }
    },

    buttons: function(next, prev) {
        if (this.o.noButtons) return;

        var carousel = this;

        if (this.o.nextButtonStateHandler != null) {
            var nextHandler = this.o.nextButtonStateHandler;
            jQuery(".jcarousel-next", this.scope).each(function() {
                nextHandler(carousel, this, next);
            });
        }

        if (this.o.prevButtonStateHandler != null) {
            var prevHandler = this.o.prevButtonStateHandler;
            jQuery(".jcarousel-prev", this.scope).each(function() {
                prevHandler(carousel, this, prev);
            });
        }

        jQuery(".jcarousel-next", this.scope)[next ? "bind" : "unbind"]("click", this.nextClick)[next ? "removeClass" : "addClass"]("jcarousel-next-disabled")[next ? "removeAttr" : "attr"]("disabled", true);
        jQuery(".jcarousel-prev", this.scope)[prev ? "bind" : "unbind"]("click", this.prevClick)[prev ? "removeClass" : "addClass"]("jcarousel-prev-disabled")[prev ? "removeAttr" : "attr"]("disabled", true);
    },

    load: function(first, last) {
        if (this.o.loadItemHandler != null) {
            this.buttons(false, false);
            // loaded() must be called by loadItemHandler()
            this.o.loadItemHandler(this, first, last, this.available(first, last));
        } else {
            this.loaded();
        }
    },

    doNext: function() {
        this.scroll((this.o.wrap && this.last == this.size) ? 1 : this.first + this.o.itemScroll);

        if (this.o.wrap || this.last < this.size) {
            this.startAuto();
        }
    },

    doPrev: function() {
        this.scroll(this.first - this.o.itemScroll);
        this.startAuto();
    },

    prepare: function(c) {
        if (c.nodeName == "UL" || c.nodeName == "OL") {
            // Full wrapping
            c = jQuery(c).addClass("jcarousel-list")
                         .wrap('<div class="jcarousel-scope"><div class="jcarousel-clip"></div></div>').parent();

            if (!this.o.noButtons) {
                var dummy = document.createElement("div");
                dummy.innerHTML = this.o.buttonPrevHTML;
                c.before(jQuery(dummy.firstChild).addClass("jcarousel-prev"));
                dummy.innerHTML = this.o.buttonNextHTML;
                c.before(jQuery(dummy.firstChild).addClass("jcarousel-next"));
            }

            // Show it, possibly hidden from browsers having javascript disabled
            return c.parent().show().get(0);
        }

        var list = jQuery("ul", c) || jQuery("ol", c);
        list.addClass("jcarousel-list");

        // Check for clip
        if (jQuery(".jcarousel-clip", c).size() == 0) {
            list.wrap('<div class="jcarousel-clip"></div>');
        }

        if (!this.o.noButtons) {
            // Check for prev
            if (jQuery(".jcarousel-prev", c).size() == 0) {
                var dummy = jQuery(document.createElement("div")).html(this.o.buttonPrevHTML).get(0);
                jQuery(".jcarousel-clip", c).before(jQuery(dummy.firstChild).addClass("jcarousel-prev"));
            }
            // Check for next
            if (jQuery(".jcarousel-next", c).size() == 0) {
                var dummy = jQuery(document.createElement("div")).html(this.o.buttonNextHTML).get(0);
                jQuery(".jcarousel-clip", c).before(jQuery(dummy.firstChild).addClass("jcarousel-next"));
            }
        }

        // Show it, possibly hidden from browsers having javascript disabled
        jQuery(c).addClass("jcarousel-scope").show();

        return c;
    },

    intval: function(v) {
        v = parseInt(v);
        return isNaN(v) ? 0 : v;
    }
};
